Mercurial > hg > charamega
changeset 0:91ecd24948de
Imported.
author | Mikhail Kryshen <mikhail@kryshen.net> |
---|---|
date | Sat, 14 Jul 2012 05:46:29 +0400 |
parents | |
children | fac1b8f35265 |
files | .hgignore Rakefile res/net/kryshen/charamega/DejaVuSans.ttf src/net/kryshen/charamega/card.mirah src/net/kryshen/charamega/field.mirah src/net/kryshen/charamega/game.mirah src/net/kryshen/charamega/ui.mirah |
diffstat | 7 files changed, 1010 insertions(+), 0 deletions(-) [+] |
line wrap: on
line diff
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.hgignore Sat Jul 14 05:46:29 2012 +0400 @@ -0,0 +1,5 @@ +syntax: regexp + +~$ +^build$ +^local_
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Rakefile Sat Jul 14 05:46:29 2012 +0400 @@ -0,0 +1,15 @@ +require 'mirah' +require 'rake/clean' + +SRC = FileList['src/**/*.mirah'] +CLEAN.include('build/**/*.class') + +task :default => [:compile] + +task :compile => SRC do + Mirah::compile '-d', 'build', *SRC +end + +task :run => [:compile] do + sh 'java -cp build:res net/kryshen/charamega/Ui' +end
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/net/kryshen/charamega/card.mirah Sat Jul 14 05:46:29 2012 +0400 @@ -0,0 +1,100 @@ +# +# Copyright 2012 Mikhail Kryshen <mikhail@kryshen.net> +# +# This file is part of Charamega. +# +# Charamega is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Charamega is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Charamega. If not, see <http://www.gnu.org/licenses/>. +# + +package net.kryshen.charamega + +class Card + + def initialize(symbol:char, time:long) + @symbol = symbol + @angle = Math.random * Math.PI - Math.PI / 2 + + @flip_interval = long(2E8) + @remove_interval = long(2E8) + + @close_time = time - @flip_interval + @match_time = @open_time = @close_time - 1 + end + + def symbol + @symbol + end + + def angle + @angle + end + + def open(time:long):void + @open_time = time unless opened? + end + + def close(time:long):void + @close_time = time if opened? + end + + def match(time:long):void + @match_time = time unless matched? + end + + def opened? + @close_time - @open_time <= 0 + end + + def matched? + @close_time - @match_time <= 0 + end + + # -1.0 - closed, 1.0 - opened. + def flip_state(time:long) + if opened? + s = 1.0 + else + s = -1.0 + end + + t = Math.max(1, time - Math.max(@close_time, @open_time)) + + return s if t >= @flip_interval + + s * (t * 2 - @flip_interval) / double(@flip_interval) + end + + # 1.0 - visible, 0.0 - removed. + def visible_state(time:long) + return 1.0 if !matched? + + t = Math.max(1, time - @match_time - @flip_interval / 2) + Math.max(0.0, 1.0 - t / double(@remove_interval)) + end + + def auto_close?(time:long) + opened? and time - @open_time > 2E9 + end + + def toString + String.valueOf(@symbol) + + if matched? + '[matched]' + elsif opened? + '[opened]' + else + '' + end + end +end
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/net/kryshen/charamega/field.mirah Sat Jul 14 05:46:29 2012 +0400 @@ -0,0 +1,243 @@ +# +# Copyright 2012 Mikhail Kryshen <mikhail@kryshen.net> +# +# This file is part of Charamega. +# +# Charamega is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Charamega is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Charamega. If not, see <http://www.gnu.org/licenses/>. +# + +package net.kryshen.charamega + +import java.awt.* +import java.awt.geom.* +import java.awt.event.* +import javax.swing.* +import java.util.List + +class Field < JComponent + + def initialize(game:Game) + @field_color = Color.WHITE + @border_color = Color.GRAY + @active_border_color = Color.DARK_GRAY + @symbol_color = Color.BLACK + @back_color = Color.LIGHT_GRAY + @face_color = Color.new(0xFFFDDD) + + @game = game + + setOpaque true + setDoubleBuffered true + + begin + source = getClass.getResource("DejaVuSans.ttf").openStream + font = Font.createFont(Font.TRUETYPE_FONT, source) + ensure + source.close unless source.nil? + end + setFont font + + enableEvents AWTEvent.MOUSE_EVENT_MASK + enableEvents AWTEvent.MOUSE_MOTION_EVENT_MASK + end + + def processMouseEvent(event) + if isEnabled + if event.getID == MouseEvent.MOUSE_PRESSED + + @hold = hit(event.getX, event.getY) + + elsif event.getID == MouseEvent.MOUSE_RELEASED and + !@hold.nil? and @hold == hit(event.getX, event.getY) + + @game.open @hold + repaint + elsif event.getID == MouseEvent.MOUSE_EXITED and + !@hovered.nil? + + @hovered = nil + repaint + end + end + + super + end + + def processMouseMotionEvent(event) + if isEnabled + h = hit(event.getX, event.getY) + + if event.getID == MouseEvent.MOUSE_DRAGGED + if h == @hold and h != @hovered + @hovered = h + repaint + end + + if h != @hold and @hold == @hovered + @hovered = nil + repaint + end + + elsif h != @hovered + @hovered = h + repaint + end + end + + super + end + + def paintComponent(g1) + g = Graphics2D(g1) + + g.setRenderingHint(RenderingHints.KEY_ANTIALIASING, + RenderingHints.VALUE_ANTIALIAS_ON) + g.setRenderingHint(RenderingHints.KEY_FRACTIONALMETRICS, + RenderingHints.VALUE_FRACTIONALMETRICS_ON) + + w = getWidth + h = getHeight + + g.setColor @field_color + g.fillRect 0, 0, w, h + + insets = getInsets + w -= insets.left + insets.right + h -= insets.top + insets.bottom + + layout = @game.layout(w, h) + time = System.nanoTime + + max_cols = Math.ceil(double(@game.cards.size) / layout.size) + rh = double(h) / layout.size + card_size = Math.min(rh, double(w) / max_cols) / 1.4 + font_size = card_size + font = g.getFont.deriveFont(float(font_size)) + frc = g.getFontRenderContext + + rm_scale = 2.0 + + outer = Rectangle2D.Double.new + inner = Rectangle2D.Double.new + border = Path2D.Double.new(Path2D.WIND_EVEN_ODD) + + save_t = g.getTransform + + y = rh / 2 + insets.top + layout.each do |row| + cw = double(w) / List(row).size + x = cw / 2 + insets.left + + List(row).each do |e| + card = Card(e) + + v = card.visible_state(time) + + # Compute area potentially affected by the card. + if v < 1.0 + paint_x = int(Math.floor(x - cw * rm_scale / 2 - 1)) + paint_y = int(Math.floor(y - rh * rm_scale / 2 - 1)) + paint_w = int(Math.ceil(cw * rm_scale + 2)) + paint_h = int(Math.ceil(rh * rm_scale + 2)) + else + paint_x = int(Math.floor(x - cw / 2 - 1)) + paint_y = int(Math.floor(y - rh / 2 - 1)) + paint_w = int(Math.ceil(cw + 2)) + paint_h = int(Math.ceil(rh + 2)) + end + + if v > 0.0 and g.hitClip(paint_x, paint_y, paint_w, paint_h) + f = card.flip_state(time) + + if v < 1.0 or (f > -1.0 and f < 1.0) + # Animation is in progress. + repaint 30, paint_x, paint_y, paint_w, paint_h + end + + g.translate x, y + + v_scale = rm_scale - Math.sqrt(v) * rm_scale + g.scale Math.abs(f) + v_scale, 1.0 + v_scale + + g.rotate card.angle * v + + outer.setRect(-card_size / 2, -card_size / 2, + card_size, card_size) + inner.setRect(1 - card_size / 2, 1 - card_size / 2, + card_size - 2, card_size - 2) + border.reset + border.append outer, false + border.append inner, false + + if f > 0 + g.setColor with_alpha(@face_color, v) + else + g.setColor with_alpha(@back_color, v) + end + + g.fill inner + + if f > 0 + g.setColor with_alpha(@symbol_color, Math.sqrt(v)) + + gv = font.createGlyphVector(frc, String.valueOf(card.symbol)) + sb = gv.getVisualBounds + + # Scale down if the glyph does not fit. + tolerance = 0.95 + scale = Math.min(inner.getWidth * tolerance / sb.getWidth, + inner.getHeight * tolerance / sb.getHeight) + save_t_2 = g.getTransform + g.scale scale, scale if scale < 1.0 + + g.drawGlyphVector(gv, + -float(sb.getX + sb.getWidth / 2), + -float(sb.getY + sb.getHeight / 2)) + + g.setTransform save_t_2 + end + + if card == @hovered + g.setColor with_alpha(@active_border_color, v) + else + g.setColor with_alpha(@border_color, v) + end + + g.fill border + + g.setTransform save_t + end + + x += cw + end + + y += rh + end + end + + private + + def hit(x:int, y:int) + insets = getInsets + size = getSize + @game.hit(x - insets.left, + y - insets.top, + size.width - insets.left - insets.right, + size.height - insets.top - insets.bottom) + end + + def with_alpha(c:Color, alpha:double) + Color.new c.getRed, c.getGreen, c.getBlue, int(alpha * 255) + end +end
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/net/kryshen/charamega/game.mirah Sat Jul 14 05:46:29 2012 +0400 @@ -0,0 +1,384 @@ +# +# Copyright 2012 Mikhail Kryshen <mikhail@kryshen.net> +# +# This file is part of Charamega. +# +# Charamega is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Charamega is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Charamega. If not, see <http://www.gnu.org/licenses/>. +# + +package net.kryshen.charamega + +import java.util.Collection +import java.util.List +import java.util.ArrayList +import java.util.Collections + +class Game + + def initialize + @symbols = ArrayList.new + + add_range 0x03B1, 0x03C9 # greek + add_all [0x0439, 0x044A, 0x0463, 0x0467] # cyrillic + add_range 0x20A0, 0x20B5 # currency + add_all [0x2190, 0x2194, 0x21CC] # arrows + add_all [0x221A, 0x221E, 0x222B, 0x222E] # math + add_all [0x2318, 0x23CE, 0x23CF, 0x23E3] + add_all [0x25A8, 0x25A9] + add_range 0x25C6, 0x25D0 + add_all [0x25D4] + add_range 0x2600, 0x261A + add_range 0x2620, 0x2630 + add_range 0x2638, 0x2672 + add_range 0x267A, 0x2689 + add_range 0x2690, 0x269C + add_all [0x26A0, 0x26A1] + add_all [0x2706, 0x2707, 0x2708, 0x2709] + add_all [0x270C, 0x270D, 0x270E] + add_all [0x2744] + + self.players = 1 + end + + def shuffle + return false if @shuffled + shuffle @cards.size / 2 + + true + end + + def shuffle(npairs:int):void + Collections.shuffle @symbols + + cards = ArrayList.new npairs + time = System.nanoTime + + @symbols.subList(0, npairs).each do |c| + cards.add Card.new Character(c).charValue, time + cards.add Card.new Character(c).charValue, time + end + + Collections.shuffle cards + @cards = cards + @layout = List(nil) + @first = @second = Card(nil) + @non_matching = 0 + @seconds = 0 + @shuffled = true + + compute_limits + end + + def players=(nplayers:int):void + @matches = int[nplayers] + @player = 0 + end + + def cards + @cards + end + + def start:void + shuffle + @shuffled = false + + @matches.length.times { |i| @matches[i] = 0 } + @player = 0 + + @start_time = System.nanoTime + @playing = true + end + + def stop:void + @playing = false + + unless @shuffled + time = System.nanoTime + delay = long(2E8 / @cards.size) + + @layout.each do |row| + List(row).each do |e| + card = Card(e) + card.open time unless card.matched? + time += delay + end + end + end + end + + # Returns list of lists of cards for each row. + def layout(w:int, h:int):List + return @layout unless @layout.nil? or @shuffled + + n = @cards.size + cols = columns(float(w) / h) + rows = int(Math.ceil float(n) / cols) + + if !@layout.nil? and + @layout_cols == cols and + @layout_rows == rows + + return @layout + end + + layout = ArrayList.new rows + + d = cols * rows - n + c = 0 + rows.times do |i| + k = cols + k -= 1 if i >= rows - d + + row = ArrayList.new k + k.times do + row.add @cards.get(c) + c += 1 + end + + layout.add row + end + + Collections.shuffle layout + + @layout_cols = cols + @layout_rows = rows + @layout = layout + end + + # Called periodically by UI. + def update + time = System.nanoTime + @seconds = int((time - @start_time) / 1E9) + + # Flip the cards back after some time. + if !@second.nil? and + @first.auto_close?(time) and @second.auto_close?(time) + + @first.close time + @second.close time + return true + end + + false + end + + # Number of removed pairs. + def matched + matched = 0 + @matches.each { |i| matched += i } + + matched + end + + def finished? + ignore_limits = false + # ignore_limits = true + + matched * 2 == @cards.size or + (!multiplayer? and !ignore_limits and + (@non_matching > @max_non_matching or + @seconds >= @max_seconds)) + end + + def winner + max = -1 + winner = -1 + + @matches.length.times do |i| + if @matches[i] > max + max = @matches[i] + winner = i + elsif @matches[i] == max + winner = -1 + end + end + + winner + end + + def multiplayer? + @matches.length > 1 + end + + def status + if multiplayer? + sb = StringBuffer.new + sb.append 'Scores: ' + @matches.length.times do |i| + sb.append ', ' if i > 0 + sb.append '(' if i == @player + sb.append String.valueOf(@matches[i]) + sb.append ')' if i == @player + end + sb.append ". " + + if finished? + w = winner + sb.append "Player #{w + 1} wins!" if w >= 0 + sb.append "Tie!" if w < 0 + else + sb.append "Player #{@player + 1}'s turn." + end + + sb.toString + else + sec = @max_seconds - @seconds + time = Integer[2] + time[0] = Integer.valueOf(sec / 60) + time[1] = Integer.valueOf(sec % 60) + + sb = StringBuffer.new + sb.append String.format("Time left: %02d:%02d.", time) + sb.append " Non-matching: #{@non_matching}" + sb.append " / #{@max_non_matching}." + + left = @cards.size / 2 - matched + + if left == 0 + sb.append " You win!" + puts "#{@cards.size / 2},#{@non_matching},#{@seconds}" + else + sb.append " Pars left: #{left}." + end + + sb.toString + end + end + + def open(card:Card):boolean + return false if finished? + return false if card.matched? + return false if card.opened? and (@first.nil? or @second.nil?) + + time = System.nanoTime + + if @first.nil? + @first = card.open time + elsif @second.nil? + @second = card.open time + else + @first.close time + @second.close time + @first = card.open time + @second = nil + end + + unless @second.nil? + if @first.symbol == @second.symbol + @first.match time + @second.match time + @first = @second = nil + @matches[@player] += 1 + else + @non_matching += 1 + @player = (@player + 1) % @matches.length + end + end + + true + end + + def hit(x:int, y:int, w:int, h:int) + return nil if x < 0 or y < 0 + + layout = layout(w, h) + i = y * layout.size / h + + return nil if i >= layout.size + + row = List(layout.get i) + j = x * row.size / w + + return nil if j >= row.size + + Card(row.get j) + end + + private + + def compute_limits + pairs = @cards.size / 2 + + @max_non_matching = 0 + @max_seconds = 2 + + # No need to optimize this. + pairs.downto(3) do |i| + if i > 180 + @max_non_matching += 9 + elsif i > 150 + @max_non_matching += 8 + elsif i > 120 + @max_non_matching += 7 + elsif i > 90 + @max_non_matching += 6 + elsif i > 60 + @max_non_matching += 5 + elsif i > 30 + @max_non_matching += 4 + elsif i > 20 + @max_non_matching += 3 + elsif i > 8 + @max_non_matching += 2 + else + @max_non_matching += 1 + end + + if i > 160 + @max_seconds += 30 + elsif i > 120 + @max_seconds += 25 + elsif i > 80 + @max_seconds += 20 + elsif i > 60 + @max_seconds += 15 + elsif i > 40 + @max_seconds += 12 + elsif i > 20 + @max_seconds += 9 + elsif i > 9 + @max_seconds += 6 + elsif i > 5 + @max_seconds += 4 + else + @max_seconds += 2 + end + end + end + + def columns(aspect:float) + n = @cards.size + c = Math.max(1, int(Math.ceil Math.sqrt(n * aspect))) + + loop do + m = n % c + break if m == 0 or c - m <= n / c + c -= 1 + end + + c + end + + def add_range(from:int, to:int):void + from.upto(to) do |i| + @symbols.add Character.new char(i) + end + end + + def add_all(xs:Collection) + xs.each do |i| + @symbols.add Character.new char(Number(i).intValue) + end + end +end
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/net/kryshen/charamega/ui.mirah Sat Jul 14 05:46:29 2012 +0400 @@ -0,0 +1,263 @@ +# +# Copyright 2012 Mikhail Kryshen <mikhail@kryshen.net> +# +# This file is part of Charamega. +# +# Charamega is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Charamega is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Charamega. If not, see <http://www.gnu.org/licenses/>. +# + +package net.kryshen.charamega + +import java.awt.* +import java.awt.event.* +import javax.swing.* +import java.net.URI + +class Ui < JPanel + @@title = 'Charamega' + @@version = '0.9' + @@home = 'http://kryshen.net/games/' + + def initialize(root_pane:JRootPane) + super LayoutManager(BorderLayout.new) + + @game = Game.new.shuffle(20) + @field = Field.new(@game) + @field.setBorder BorderFactory.createEmptyBorder(5, 5, 5, 5) + + add @field, BorderLayout.CENTER + add create_status, BorderLayout.SOUTH + + ui = self + @timer = Timer.new(250) { |e| ui.update } + + @conf = create_configurator + root_pane.getLayeredPane.add @conf, JLayeredPane.MODAL_LAYER + root_pane.setDefaultButton @start + + stop + update_status + end + + def doLayout + @conf.setSize @conf.getPreferredSize + @conf.setLocation int(getWidth * 0.05), int(getHeight * 0.05) + super + end + + def update + @field.repaint if @game.update + stop if @game.finished? + update_status + end + + def update_status + @status.setText(@game.status) + end + + def start + @conf.setVisible false + @field.setEnabled true + @start.setEnabled false + @stop.setEnabled true + @game.start + @timer.start + @field.repaint + end + + def stop + @timer.stop + @game.stop + @stop.setEnabled false + @start.setEnabled true + @field.setEnabled false + @conf.setVisible true + @field.repaint + end + + private + + def create_status + ui = self + + status_panel = JPanel.new + layout = GroupLayout.new(status_panel) + status_panel.setLayout layout + + @status = JLabel.new('', SwingConstants.RIGHT) + @stop = JButton.new('New game') + @stop.addActionListener do |e| + ui.update_status + ui.stop + end + + gap = 5 + + h_group = layout.createSequentialGroup + .addGap(1, gap, gap) + .addComponent(@stop) + .addGap(1, gap, gap) + .addComponent(@status, + GroupLayout.DEFAULT_SIZE, + GroupLayout.DEFAULT_SIZE, + Short.MAX_VALUE) + .addGap(1, gap, gap) + + v_group = layout.createSequentialGroup + .addGap(1, gap, gap) + .addGroup(layout.createParallelGroup(GroupLayout.Alignment.BASELINE) + .addComponent(@stop) + .addComponent(@status)) + .addGap(1, gap, gap) + + layout.setHorizontalGroup h_group + layout.setVerticalGroup v_group + + status_panel + end + + def create_configurator + # Can't use object fields in closures in Mirah 0.11. + ui = self + game = @game + field = @field + + conf = JPanel.new + layout = GroupLayout.new(conf) + conf.setLayout layout + conf.setBorder BorderFactory.createRaisedBevelBorder + + layout.setAutoCreateGaps true + layout.setAutoCreateContainerGaps true + + title = JLabel.new(@@title) + .setFont(Font.new('serif', Font.BOLD, 26)) + + version = JLabel.new("version #{@@version}") + link = HyperlinkLabel.new(@@home) + + players_label = JLabel.new('Players: ') + pairs_label = JLabel.new('Pairs: ') + + players = SpinnerNumberModel.new(1, 1, 20, 1) + pairs = SpinnerNumberModel.new(20, 2, 200, 5) + + players_spinner = JSpinner.new(players) + pairs_spinner = JSpinner.new(pairs) + + players.addChangeListener do |e| + game.players = players.getNumber.intValue + field.repaint if game.shuffle + ui.update_status + end + + pairs.addChangeListener do |e| + game.shuffle pairs.getNumber.intValue + field.repaint + ui.update_status + end + + @start = JButton.new("Start") + @start.addActionListener do |e| + ui.start + end + + h_group = layout.createParallelGroup + .addGroup(layout.createSequentialGroup + .addComponent(title) + .addComponent(version)) + .addGroup(layout.createSequentialGroup + .addComponent(link, + GroupLayout.DEFAULT_SIZE, + GroupLayout.DEFAULT_SIZE, + GroupLayout.PREFERRED_SIZE) + .addGap(0, 0, Short.MAX_VALUE)) + .addGroup(layout.createSequentialGroup + .addGap(0, 0, Short.MAX_VALUE) + .addGroup(layout.createParallelGroup + .addComponent(pairs_label) + .addComponent(players_label)) + .addGroup(layout.createParallelGroup + .addComponent(pairs_spinner) + .addComponent(players_spinner)) + .addGap(0, 0, Short.MAX_VALUE)) + .addGroup(layout.createSequentialGroup + .addGap(0, 0, Short.MAX_VALUE) + .addComponent(@start)) + + v_group = layout.createSequentialGroup + .addGroup(layout.createBaselineGroup(false, true) + .addComponent(title) + .addComponent(version)) + .addComponent(link) + .addGap(25) + .addGroup(layout.createBaselineGroup(false, true) + .addComponent(pairs_label) + .addComponent(pairs_spinner)) + .addGroup(layout.createBaselineGroup(false, true) + .addComponent(players_label) + .addComponent(players_spinner)) + .addGap(15) + .addComponent(@start) + + layout.setHorizontalGroup h_group + layout.setVerticalGroup v_group + + conf + end +end + +class HyperlinkLabel < JLabel + + def initialize(uri:String) + initialize URI.new(uri), uri + end + + def initialize(uri:URI) + initialize uri, uri.toString + end + + def initialize(uri:URI, text:String) + @uri = uri + + s = text.replace('&', '&') + .replace('<', '<') + .replace('>', '>') + + setText "<html><u>#{s}</u></html>" + setForeground Color.BLUE.darker + setCursor Cursor.new(Cursor.HAND_CURSOR) + + enableEvents AWTEvent.MOUSE_EVENT_MASK + end + + def processMouseEvent(event) + if event.getID == MouseEvent.MOUSE_CLICKED + Desktop.getDesktop.browse @uri + end + + super + end +end + +# UIManager.setLookAndFeel UIManager.getSystemLookAndFeelClassName + +frame = JFrame.new 'Charamega' +ui = Ui.new(frame.getRootPane) + +frame.getContentPane.add ui +frame.setDefaultCloseOperation(WindowConstants.EXIT_ON_CLOSE) + .setSize(800, 600) + .validate + .setVisible(true)