Newer
Older
<?xml version="1.0" encoding="UTF-8"?>
<chapter id="clipboardtut">
<title>Working With the Clipboard</title>
<para>Two of the standard FOX widgets, <classname>FXText</classname> and
<classname>FXTextField</classname>, provide clipboard support out of the
box. For example, you can select some text in an
<classname>FXTextField</classname> and then press Ctrl+C to copy that text
to the system clipboard. You can also press Ctrl+X to "cut" the selected
text to the clipboard, or Ctrl+V to paste text from the clipboard into an
<classname>FXText</classname> or <classname>FXTextField</classname> widget.
The purpose of this tutorial is to demonstrate how to interact with the
clipboard programmatically, so that you can integrate additional clipboard
support into your FXRuby applications.</para>
<section>
<title>Basic Application</title>
<para>In order to illustrate how to integrate cut and paste operations
into your application, we'll start from a simple FXRuby application that
doesn't yet provide any clipboard support. This application simply
presents a list of customers (from some external source).</para>
<programlisting format="linespecific">require 'fox16'
require 'customer'
include Fox
class ClipMainWindow < FXMainWindow
def initialize(anApp)
# Initialize base class first
super(anApp, "Clipboard Example", :opts => DECOR_ALL, :width => 400, :height => 300)
sunkenFrame = FXVerticalFrame.new(self,
LAYOUT_FILL_X|LAYOUT_FILL_Y|FRAME_SUNKEN|FRAME_THICK, :padding => 0)
customerList = FXList.new(sunkenFrame, :opts => LIST_BROWSESELECT|LAYOUT_FILL_X|LAYOUT_FILL_Y)
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
$customers.each do |customer|
customerList.appendItem(customer.name, nil, customer)
end
end
def create
super
show(PLACEMENT_SCREEN)
end
end
if __FILE__ == $0
FXApp.new("ClipboardExample", "FXRuby") do |theApp|
ClipMainWindow.new(theApp)
theApp.create
theApp.run
end
end
</programlisting>
<para>We're assuming that the "customer" module defines a
<classname>Customer</classname> class and a global array
<varname>$customers</varname> that contains the list of customers. For a
real world application, you might access this information from a database
or some other source, but for this example we'll just use a hard-coded
array:</para>
<programlisting format="linespecific"># customer.rb
Customer = Struct.new("Customer", :name, :address, :zip)
$customers = []
$customers << Customer.new("Reed Richards", "123 Maple, Central City, NY", 010111)
$customers << Customer.new("Sue Storm", "123 Maple, Anytown, NC", 12345)
$customers << Customer.new("Benjamin J. Grimm", "123 Maple, Anytown, NC", 12345)
$customers << Customer.new("Johnny Storm", "123 Maple, Anytown, NC", 12345)
</programlisting>
<para>The goals for the next few sections are to extend this application
so that users can select a customer from the list and copy that customer's
information to the clipboard, and subsequently paste that information into
another copy of the program (or some other clipboard-aware
application).</para>
</section>
<section>
<title>Acquiring the Clipboard</title>
<para>Let's begin by augmenting the GUI to include a row of buttons along
the bottom of the main window for copying and pasting:</para>
<programlisting format="linespecific">require 'fox16'
require 'customer'
include Fox
class ClipMainWindow < FXMainWindow
def initialize(anApp)
# Initialize base class first
super(anApp, "Clipboard Example", :opts => DECOR_ALL, :width => 400, :height => 300)
<emphasis role="bold">
# Horizontal frame contains buttons
buttons = FXHorizontalFrame.new(self, LAYOUT_SIDE_BOTTOM|LAYOUT_FILL_X|PACK_UNIFORM_WIDTH)
</emphasis><emphasis role="bold">
# Cut and paste buttons
copyButton = FXButton.new(buttons, "Copy")
pasteButton = FXButton.new(buttons, "Paste")
</emphasis>
# Place the list in a sunken frame
sunkenFrame = FXVerticalFrame.new(self,
LAYOUT_FILL_X|LAYOUT_FILL_Y|FRAME_SUNKEN|FRAME_THICK, :padding => 0)
customerList = FXList.new(sunkenFrame, :opts => LIST_BROWSESELECT|LAYOUT_FILL_X|LAYOUT_FILL_Y)
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
$customers.each do |customer|
customerList.appendItem(customer.name, nil, customer)
end
end
def create
super
show(PLACEMENT_SCREEN)
end
end
if __FILE__ == $0
FXApp.new("ClipboardExample", "FXRuby") do |theApp|
ClipMainWindow.new(theApp)
theApp.create
theApp.run
end
end
</programlisting>
<para>Note that the lines which appear in bold face are those which have
been added (or changed) since the previous source code listing.</para>
<para>The clipboard is a kind of shared resource in the operating system.
Copying (or cutting) data to the clipboard begins with some window in your
application requesting "ownership" of the clipboard by calling the
<methodname>acquireClipboard()</methodname> instance method. Let's add a
handler for the "Copy" button press which does just that:</para>
<programlisting format="linespecific"># User clicks Copy
copyButton.connect(SEL_COMMAND) do
customer = customerList.getItemData(customerList.currentItem)
types = [ FXWindow.stringType ]
if acquireClipboard(types)
@clippedCustomer = customer
end
end
</programlisting>
<para>The <methodname>acquireClipboard()</methodname> method takes as its
input an array of drag types. A <emphasis>drag type</emphasis> is just a
unique value, assigned by the window system, that identifies a particular
kind of data. In this case, we're using one of FOX's pre-registered drag
types (<constant>stringType</constant>) to indicate that we have some
string data to place on the clipboard. Later, we'll see how to register
customized, application-specific drag types as well.</para>
<para>The <methodname>acquireClipboard()</methodname> method returns
<constant>true</constant> on success; since we called
<methodname>acquireClipboard()</methodname> on the main window, this means
that the main window is now the clipboard owner. At this time, we want to
save a reference to the currently selected customer in the
<varname>@clippedCustomer</varname> instance variable so that if its value
is requested later, we'll be able to return the
<emphasis>correct</emphasis> customer's information.</para>
</section>
<section>
<title>Sending Data to the Clipboard</title>
<para>Whenever some other window requests the clipboard's contents (e.g.
as a result of a "paste" operation) FOX will send a
<constant>SEL_CLIPBOARD_REQUEST</constant> message to the current
clipboard owner. Remember, the clipboard owner is the window that called
<methodname>acquireClipboard()</methodname>. For our example, the main
window is acting as the clipboard owner and so it needs to handle the
<constant>SEL_CLIPBOARD_REQUEST</constant> message:</para>
<programlisting format="linespecific"># Handle clipboard request
self.connect(SEL_CLIPBOARD_REQUEST) do
setDNDData(FROM_CLIPBOARD, FXWindow.stringType, Fox.fxencodeStringData(@clippedCustomer.to_s))
end
</programlisting>
<para>The <methodname>setDNDData()</methodname> method takes three
arguments. The first argument tells FOX which kind of data transfer we're
trying to accomplish; as it turns out, this method can be used for
drag-and-drop (<constant>FROM_DRAGNDROP</constant>) and X11 selection
(<constant>FROM_SELECTION</constant>) data transfer as well. The second
argument to <methodname>setDNDData()</methodname> is the drag type for the
data and the last argument is the data itself, a binary string.</para>
<para>If you're wondering why we need to call the
<methodname>fxencodeStringData()</methodname> module method to preprocess
the string returned by the call to <methodname>Customer#to_s</methodname>,
that's a reasonable thing to wonder about. In order for FOX to play nice
with other clipboard-aware applications, it must be able to store string
data on the clipboard in the format expected by those applications.
Unfortunately, that expected format is platform-dependent and does not
always correspond directly to the format that Ruby uses internally to
store its string data. The <methodname>fxencodeStringData()</methodname>
method (and the corresponding
<methodname>fxdecodeStringData()</methodname> method) provide you with a
platform-independent way of sending (or receiving) string data with the
<constant>stringType</constant> drag type.</para>
<para>If you run the program as it currently stands, you should now be
able to select a customer from the list, click the "Copy" button and then
paste the selected customer data (as a string) into some other
application. For example, if you're trying this tutorial on a Windows
machine, try pasting into a copy of Notepad or Microsoft Word. The pasted
text should look something like:</para>
<programlisting format="linespecific">#<struct Struct::Customer name="Joe Smith", address="123 Maple, Anytown, NC", zip=12345>
</programlisting>
</section>
<section>
<title>Pasting Data from the Clipboard</title>
<para>We've seen one side of the equation, copying string data to the
clipboard. But before we can "round-trip" that customer data and paste it
back into another copy of our customer list application, we're clearly
going to need to transfer the data in some more useful format. That is to
say, if we were to receive the customer data in the format that it's
currently stored on the clipboard:</para>
<programlisting format="linespecific">#<struct Struct::Customer name="Joe Smith", address="123 Maple, Anytown, NC", zip=12345>
</programlisting>
<para>We'd have to parse that string and try to extract the relevant data
from it. We can do better than that. The approach we'll use instead is to
serialize and deserialize the objects using YAML. First, make sure that
the YAML module is loaded by adding this line:</para>
<programlisting format="linespecific">require 'yaml'</programlisting>
<para>somewhere near the top of the program. Next, register a custom drag
type for <classname>Customer</classname> objects. We can do that by adding
one line to our main window's <methodname>create</methodname> instance
method:</para>
<programlisting format="linespecific">def create
super
<emphasis role="bold"> @customerDragType = getApp().registerDragType("application/x-customer")
</emphasis> show(PLACEMENT_SCREEN)
end
</programlisting>
<para>Note that by convention, the name of the drag type is the MIME type
for the data, but any unique string will do. In our case, we'll use the
string "application/x-customer" to identify the drag type for our
YAML-serialized <classname>Customer</classname> objects.</para>
<para>With that in place, we can now go back and slightly change some of
our previous code. When we acquire the clipboard, we'd now like to be able
to offer the selected customer's information either as plain text (i.e.
the previous format) <emphasis>or</emphasis> as a YAML document, so we'll
include <emphasis>both</emphasis> of these types in the array of drag
types passed to <methodname>acquireClipboard()</methodname>:</para>
<programlisting format="linespecific"># User clicks Copy
copyButton.connect(SEL_COMMAND) do
customer = customerList.getItemData(customerList.currentItem)
<emphasis role="bold"> types = [ FXWindow.stringType, @customerDragType ]
</emphasis> if acquireClipboard(types)
@clippedCustomer = customer
end
end
</programlisting>
<para>Similarly, when we're handling the
<constant>SEL_CLIPBOARD_REQUEST</constant> message, we now need to pay
attention to which drag type (i.e. which data format) the requestor
specified. We can do that by inspecting the
<methodname>target</methodname> attribute of the
<classname>FXEvent</classname> instance passed along with the
<constant>SEL_CLIPBOARD_REQUEST</constant> message:</para>
<programlisting format="linespecific"># Handle clipboard request
self.connect(SEL_CLIPBOARD_REQUEST) do |sender, sel, event|
case event.target
when FXWindow.stringType
setDNDData(FROM_CLIPBOARD, FXWindow.stringType, Fox.fxencodeStringData(@clippedCustomer.to_s))
when @customerDragType
setDNDData(FROM_CLIPBOARD, @customerDragType, @clippedCustomer.to_yaml)
else
# Ignore requests for unrecognized drag types
end
end
</programlisting>
<para>With these changes in place, we can now add a handler for the
"Paste" button which requests the clipboard data in YAML format,
deserializes it, and then adds an item to the customer list:</para>
<programlisting format="linespecific"># User clicks Paste
pasteButton.connect(SEL_COMMAND) do
data = getDNDData(FROM_CLIPBOARD, @customerDragType)
if data
customer = YAML.load(data)
customerList.appendItem(customer.name, nil, customer)
end
end
</programlisting>
<para>The <methodname>getDNDData()</methodname> method used here is the
inverse of the <methodname>setDNDData()</methodname> method we used
earlier to push data to some other application requesting our clipboard
data. As with <methodname>setDNDData()</methodname>, the arguments to
<methodname>getDNDData()</methodname> indicate the kind of data transfer
we're performing (e.g. <constant>FROM_CLIPBOARD</constant>) and the drag
type for the data we're requesting. If some failure occurs (usually,
because the clipboard owner can't provide its data in the requested
format) <methodname>getDNDData()</methodname> will simply return
<constant>nil</constant>.</para>
</section>
</chapter>